import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.StructLayout;
import java.lang.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;
import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;
import static java.lang.foreign.ValueLayout.JAVA_LONG;

/**
 * <p>compile with: -J-Duser.language=en
 * <p>run with:     --enable-native-access=ALL-UNNAMED
 *
 * @see <a href="https://openjdk.org/jeps/434">FFI reference</a>
 */
@SuppressWarnings({"resource", "OptionalGetWithoutIsPresent"})
public class TestGmp {
	public static final StructLayout mpz_struct;
	public static final MethodHandle mpz_init;
	public static final MethodHandle mpz_init_set_si;
	public static final MethodHandle mpz_init_set_ui;
	public static final MethodHandle mpz_clear;
	public static final MethodHandle mpz_realloc;
	public static final MethodHandle mpz_get_si;
	public static final MethodHandle mpz_get_ui;
	public static final MethodHandle mpz_set_si;
	public static final MethodHandle mpz_set_ui;
	public static final MethodHandle mpz_add;
	public static final MethodHandle mpz_addmul_ui;
	public static final MethodHandle mpz_submul_ui;
	public static final MethodHandle mpz_mul_ui;
	public static final MethodHandle mpz_tdiv_q;
	public static final MethodHandle mpz_cmp;

	static {
		// https://gmplib.org/
		// need MinGW-w64, msys-1.0
		// export ABI=64
		// export CC=x86_64-w64-mingw32-gcc
		// ./configure --build=x86_64-w64-mingw32 --host=x86_64-w64-mingw32 --disable-static --enable-shared --enable-fat
		// make
		var libGmp = SymbolLookup.libraryLookup("libgmp-10.dll", Arena.global());
		var linker = Linker.nativeLinker();

		mpz_struct = MemoryLayout.structLayout(
				JAVA_INT.withName("mp_alloc"), // alloc count of mp_limb_t in mp_d
				JAVA_INT.withName("mp_size"), // current used count of mp_limb_t in mp_d
				ADDRESS.withName("mp_d") // mp_limb_t* (GMP_LIMB_BITS=64)
		);

		mpz_init = linker.downcallHandle(libGmp.find("__gmpz_init").get(), // must call mpz_clear before init again
				FunctionDescriptor.ofVoid(ADDRESS));
		mpz_init_set_si = linker.downcallHandle(libGmp.find("__gmpz_init_set_si").get(),
				FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG));
		mpz_init_set_ui = linker.downcallHandle(libGmp.find("__gmpz_init_set_ui").get(),
				FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG));
		mpz_clear = linker.downcallHandle(libGmp.find("__gmpz_clear").get(), // not safe if call twice, then init again
				FunctionDescriptor.ofVoid(ADDRESS));
		mpz_realloc = linker.downcallHandle(libGmp.find("__gmpz_realloc").get(), // must call after init
				FunctionDescriptor.ofVoid(ADDRESS));
		mpz_get_si = linker.downcallHandle(libGmp.find("__gmpz_get_si").get(), // only lowest (GMP_LIMB_BITS-1) bits
				FunctionDescriptor.of(JAVA_LONG, ADDRESS));
		mpz_get_ui = linker.downcallHandle(libGmp.find("__gmpz_get_ui").get(), // only lowest GMP_LIMB_BITS bits
				FunctionDescriptor.of(JAVA_LONG, ADDRESS));
		mpz_set_si = linker.downcallHandle(libGmp.find("__gmpz_set_si").get(),
				FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG));
		mpz_set_ui = linker.downcallHandle(libGmp.find("__gmpz_set_ui").get(),
				FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG));

		mpz_add = linker.downcallHandle(libGmp.find("__gmpz_add").get(),
				FunctionDescriptor.ofVoid(ADDRESS, ADDRESS, ADDRESS));
		mpz_addmul_ui = linker.downcallHandle(libGmp.find("__gmpz_addmul_ui").get(),
				FunctionDescriptor.ofVoid(ADDRESS, ADDRESS, JAVA_LONG));
		mpz_submul_ui = linker.downcallHandle(libGmp.find("__gmpz_submul_ui").get(),
				FunctionDescriptor.ofVoid(ADDRESS, ADDRESS, JAVA_LONG));
		mpz_mul_ui = linker.downcallHandle(libGmp.find("__gmpz_mul_ui").get(),
				FunctionDescriptor.ofVoid(ADDRESS, ADDRESS, JAVA_LONG));
		mpz_tdiv_q = linker.downcallHandle(libGmp.find("__gmpz_tdiv_q").get(),
				FunctionDescriptor.ofVoid(ADDRESS, ADDRESS, ADDRESS));
		mpz_cmp = linker.downcallHandle(libGmp.find("__gmpz_cmp").get(),
				FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS));
	}

	static final MemorySegment tmp1_Pointer = Arena.global().allocate(mpz_struct);
	static final MemorySegment tmp2_Pointer = Arena.global().allocate(mpz_struct);
	static final MemorySegment acc_Pointer = Arena.global().allocate(mpz_struct);
	static final MemorySegment den_Pointer = Arena.global().allocate(mpz_struct);
	static final MemorySegment num_Pointer = Arena.global().allocate(mpz_struct);

	static long extract_Digit(final long nth) throws Throwable {
		// joggling between tmp1_Pointer and tmp2_Pointer, so GMP won't have to
		// use temp buffers
		mpz_mul_ui.invoke(tmp1_Pointer, num_Pointer, nth);
		mpz_add.invoke(tmp2_Pointer, tmp1_Pointer, acc_Pointer);
		mpz_tdiv_q.invoke(tmp1_Pointer, tmp2_Pointer, den_Pointer);

		return (long)mpz_get_ui.invoke(tmp1_Pointer);
	}

	static void eliminate_Digit(final long d) throws Throwable {
		mpz_submul_ui.invoke(acc_Pointer, den_Pointer, d);
		mpz_mul_ui.invoke(acc_Pointer, acc_Pointer, 10);
		mpz_mul_ui.invoke(num_Pointer, num_Pointer, 10);
	}

	static void next_Term(final long k) throws Throwable {
		final long k2 = k * 2 + 1;
		mpz_addmul_ui.invoke(acc_Pointer, num_Pointer, 2);
		mpz_mul_ui.invoke(acc_Pointer, acc_Pointer, k2);
		mpz_mul_ui.invoke(den_Pointer, den_Pointer, k2);
		mpz_mul_ui.invoke(num_Pointer, num_Pointer, k);
	}

	public static void testBench() throws Throwable {
		final long n = 10000;

		mpz_init_set_ui.invoke(tmp1_Pointer, 0);
		mpz_init_set_ui.invoke(tmp2_Pointer, 0);
		mpz_init_set_ui.invoke(acc_Pointer, 0);
		mpz_init_set_ui.invoke(den_Pointer, 1);
		mpz_init_set_ui.invoke(num_Pointer, 1);

		final StringBuilder results = new StringBuilder();

		for (long i = 0, k = 0; i < n; ) {
			next_Term(++k);
			if ((int)mpz_cmp.invoke(num_Pointer, acc_Pointer) > 0)
				continue;

			final long d = extract_Digit(3);
			if (d != extract_Digit(4))
				continue;

			results.append(d);
			if (++i % 10 == 0)
				results.append("\t:").append(i).append("\n");
			eliminate_Digit(d);
		}

		System.out.print(results);
	}

	public static void main(String[] args) throws Throwable {
		testBench();

		var ms = Arena.ofAuto().allocate(mpz_struct);
		mpz_init_set_si.invoke(ms, 1234);
		System.out.println(mpz_get_si.invoke(ms));
		mpz_clear.invoke(ms);
		System.out.println("end");
	}
}
